# 의존성을 대체하는 테스트 더블

# 기존 테스트의 문제

테스트 더블은 테스트할 로직에서 의존하고 있는 객체를 대체해주는 객체입니다.
위의 예시 코드 중 login() 함수 내 로직을 다시 보죠.

def login(user_id: str, user_password: str) -> str:
    user_repository = UserRepository()  # DB와 연동되어 User 정보를 저장하고 불러오는 객체
    user = user_repository.find_by_id(user_id)
    if user.id == user_id and user.password == user_password:
        return create_token(user_id)
    else:
        raise Exception("로그인 인증에 실패했습니다.")

위 코드는 UserRepository 객체를 의존하고 있습니다. UserRepository 객체는 DB와 연결을 맺어 데이터를 저장하고, 불러오는 객체로 DB가 먼저 실행된 상태여야 정상적으로 작동합니다. 즉 UserRepository 객체는 외부 DB에 의존성이 있습니다.

따라서 위 login() 함수를 정상적으로 테스트하려면 DB가 어딘가에 실행된 상태여야 하고, UserRepository 역시 문제없이 잘 작동하는 상태여야 합니다. 이처럼 로직이 다른 객체들과 외부 컴포넌트(DB 등)을 의존하게 되면 테스트를 실행하는데 신경 써야 할 것들이 생기게 됩니다. 단적으로 DB가 어딘가에서 실행되어 있지 않으면 작성했던 통합 테스트 코드를 정상적으로 실행시킬 수 없습니다.

# 테스트 더블 적용하기

테스트 더블은 이런 의존성 객체들을 "대체"함으로써 테스트를 좀 더 원활하게 진행하기 위한 객체입니다.
예를 들어 위에서 UserRepository 객체는 테스트 코드에서 다음과 같은 FakeRepository 라는 페이크 객체로 대체할 수 있습니다.

class FakeRepository(Repository):
    """ DB를 이용하지 않고, 인메모리로 데이터를 저장하고 불러냅니다."""
    
    def __init__(self, data: Dict[str, User]) -> None:
        self._data = data
        
    def find_by_id(id: str) -> Optional[User]:
        return self._data.get(id, None)

login() 함수를 좀 더 테스트하기 쉽게 만들기 위해, 의존하는 객체를 함수 내부에서 직접 생성하지 않고, 외부에서 파라미터로 주입받도록 수정합니다.

def login(user_id: str, user_password: str, repository: Repository) -> str:  # repository 파라미터를 추가합니다.
    user = repository.find_by_id(user_id)
    if user_id == user.id and user.password == user_password:
        return create_token(user_id)
    else:
        raise Exception("로그인 인증에 실패했습니다.")

이제 테스트 코드는 다음처럼 FakeRepository 를 이용하여 작성할 수 있습니다.

def test_login_successful():
    # given
    repository = FakeRepository(data={  # 테스트 더블 객체를 만듭니다.
        "grab": {
            "id": "grab",
            "password": "1234"
        }
    })
    user_id = "grab"
    user_password = "1234"
    
    # when
    actual = login(user_id, user_password, repository)  # 테스트 더블 객체를 주입합니다.
    
    # then
    assert actual == "grab_verified"

image-20210915215215481

이제 테스트 코드는 DB에 대한 의존성이 없는 상태로 테스트가 가능합니다. 위 예시 코드에서 우리가 사용한 테스트 더블은 fake object입니다.

# 테스트 더블의 종류

위 테스트에서는 외부 의존성을 대체하기 위해 테스트 더블 중 하나인 페이크 객체로 구현하였습니다.
테스트 더블은 이 외에도 대표적으로 다음과 같은 종류가 있습니다.

# dummy

  • 실제 내부 동작은 구현하지 않은 채, 객체의 인터페이스만 구현한 테스트 더블 객체입니다.
  • 메서드가 동작하지 않아도 테스트에 문제가 없을 때 사용합니다.
class DummyRepository(Repository):
    def insert(self, data):
        return True
    
    def find_by_id(self, user_id):
        return "grab"

# stub

  • dummy 테스트 더블 객체에서 테스트에 필요한 최소한의 구현만 해둔 테스트 더블 객체입니다.
  • 테스트에서 호출될 요청에 대해 미리 준비해둔 결과만을 반환합니다.
class StubUserRepositry(Repository):
    def insert(self, data):
        return "OK"

    def findById(self, user_id):
        return {"id": user_id, "name": "test_grab", ...}

    ...

# spy

  • stub에서 테스트에 필요한 정보를 기록해두는 테스트 더블 객체입니다.
  • 보통 stub의 역할을 포함합니다
  • 실제로 내부가 잘 동작했는지 등을 별도의 인스턴스 변수로 기록해둡니다.
class SpyUserRepositry(Repository):
    insert_called=0
   
    def insert(self, data):
        SpyUserRepositry.insert_called += 1
        return "OK"
   
    @property
    def get_insert_called(self):
        return SpyUserRepositry.insert_called

    ...

# fake

  • 동작의 구현은 갖추고 있지만, 테스트에서만 사용할 수 있는 테스트 더블 객체입니다.
  • 대체할 객체가 복잡한 내부 로직이나 외부 의존성이 있을 때 사용합니다.
class FakeUserRepository(Repository): 
    def __init__(self): 
        self.users = []

    def insert(self, data):
        self.users.append(data)

    def find_by_id(self, user_id):
        return [user for user in self.users if user.id == user_id]

# mock

  • 테스트에 필요한 인터페이스와 반환 값을 제공해주는 객체입니다.
  • 해당 메서드가 제대로 호출됐는지를 확인하는 행위 검증의 기능을 가집니다.
  • 다른 테스트 더블과 다르게 보통은 객체를 직접 정의하지 않고, 보통 Mock 객체로 반환 값을 미리 지정해둡니다.
    • 대부분의 테스트 프레임워크는 Mocking을 정밀하게 할 수 있도록 지원해줍니다.
@mock.patch.object(UserRepository, 'insert')
def test(insert_method):
    insert_method.return_value = "OK" #stub처럼 기대값을 반환합니다.
    insert_method({"id": 1, "name": "grab"}) 
    insert_method.assert_called_once() #해당 메서드가 호출되었는지를 확인한다(행위 검증)

#서드 파티 라이브러리에 mocking하는 사례를 추가했습니다.
@patch("requests.get")
def test_get_user(mock_get):
    response = mock_get.return_value #해당 mock 객체를 받아서 자유롭게 mocking합니다.
    response.status_code = 200
    response.json.return_value = { 
        "name" :  "Test User",
        "email" : "user@test.com"
    }
    user = get_user(1)
	
    assert user["name"] == "Test User" 
    # 해당 메서드와 인자가 제대로 불렸는지 행위를 검증합니다.
    mock_get.assert_called_once_with("https://api-server.com/users/1")  

TIP

테스트 더블의 종류를 외울 필요는 절대 없습니다. 서로 개념적으로 비슷한 부분들이 많기 때문에 현업에서도 여러 용어로 부르곤 합니다.

Last Updated: 2/20/2022, 1:51:31 PM

CC-BY-NC-ND-4.0 Licensed | Copyright © 2021-present Grab